﻿using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CodeActions;
using Microsoft.CodeAnalysis.CodeFixes;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Diagnostics;
using Microsoft.CodeAnalysis.Formatting;
using Microsoft.CodeAnalysis.Text;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace CodeAnalysisRuleCollection.Maths.CSharp
{
    /// <example>a + 0</example>
    [DiagnosticAnalyzer]
    [ExportDiagnosticAnalyzer(DiagnosticId, LanguageNames.CSharp)]
    internal class UnnecessaryBinaryOperationDiagnosticAnalyzer : ISyntaxNodeAnalyzer<SyntaxKind>
    {
        public const string DiagnosticId = "UnnecessaryBinaryOperation";

        public static readonly DiagnosticDescriptor Rule = new DiagnosticDescriptor(
            DiagnosticId,
            "Unnecessary binary operation",
            "The operation does not change the value of the expression.",
            "Performance",
            DiagnosticSeverity.Warning
        );

        public ImmutableArray<SyntaxKind> SyntaxKindsOfInterest
        {
            get
            {
                return ImmutableArray.Create(
                    SyntaxKind.AddExpression,
                    SyntaxKind.SubtractExpression,
                    SyntaxKind.MultiplyExpression,
                    SyntaxKind.DivideExpression
                );
            }
        }

        public ImmutableArray<DiagnosticDescriptor> SupportedDiagnostics { get { return ImmutableArray.Create(Rule); } }

        public void AnalyzeNode(SyntaxNode node, SemanticModel semanticModel, Action<Diagnostic> addDiagnostic, CancellationToken cancellationToken)
        {
            if ((var binary = node as BinaryExpressionSyntax) != null)
            {
                Optional<object>
                    leftValue = semanticModel.GetConstantValue(binary.Left, cancellationToken),
                    rightValue = semanticModel.GetConstantValue(binary.Right, cancellationToken)
                ;

                bool
                    isLeftZero = leftValue.HasValue && leftValue.Value.Equals(0),
                    isRightZero = rightValue.HasValue && rightValue.Value.Equals(0),
                    isLeftOne = leftValue.HasValue && leftValue.Value.Equals(1),
                    isRightOne = rightValue.HasValue && rightValue.Value.Equals(1)
                ;

                // a + 0, a - 0, 0 + a, 0 - a
                if (
                    (binary.IsKind(SyntaxKind.AddExpression) || binary.IsKind(SyntaxKind.SubtractExpression)) &&
                    (isLeftZero || isRightZero)
                )
                    addDiagnostic(Diagnostic.Create(Rule, node.GetLocation()));


                // a * 1, a / 1, 1 * a
                if (
                    (binary.IsKind(SyntaxKind.MultiplyExpression) && (isLeftOne || isRightOne)) ||
                    (binary.IsKind(SyntaxKind.DivideExpression) && isRightOne) ||
                    (binary.IsKind(SyntaxKind.MultiplyExpression) && (isLeftZero || isRightZero)) ||
                    (binary.IsKind(SyntaxKind.DivideExpression) && isLeftZero)
                )
                    addDiagnostic(Diagnostic.Create(Rule, node.GetLocation()));
            }
        }
    }

    [ExportCodeFixProvider(UnnecessaryBinaryOperationDiagnosticAnalyzer.DiagnosticId, LanguageNames.CSharp)]
    internal class UnnecessaryBinaryOperationCodeFixProvider : ICodeFixProvider
    {
        public IEnumerable<string> GetFixableDiagnosticIds()
        {
            return new[] { UnnecessaryBinaryOperationDiagnosticAnalyzer.DiagnosticId };
        }

        public async Task<IEnumerable<CodeAction>> GetFixesAsync(Document document, TextSpan span, IEnumerable<Diagnostic> diagnostics, CancellationToken cancellationToken)
        {
            var root = await document.GetSyntaxRootAsync(cancellationToken);

            var diagnosticSpan = diagnostics.First().Location.SourceSpan;

            // Find the local declaration identified by the diagnostic.
            var binary = root.FindToken(diagnosticSpan.Start).Parent.AncestorsAndSelf().OfType<BinaryExpressionSyntax>().First();

            // return a code action that will invoke the fix.
            return new[] { CodeAction.Create("Remove unnecessary operand", c => Fix(document, binary, c)) };
        }

        private async Task<Document> Fix(Document document, BinaryExpressionSyntax binary, CancellationToken cancellationToken)
        {
            SyntaxNode syntaxRoot = await document.GetSyntaxRootAsync(cancellationToken);
            SemanticModel model = await document.GetSemanticModelAsync(cancellationToken);

            // get simplified value
            SyntaxNode replacement = GetSimplifiedValue(model, binary, cancellationToken)
                .WithAdditionalAnnotations(Formatter.Annotation);

            SyntaxNode newRoot = syntaxRoot.ReplaceNode(binary, replacement);
            return document.WithSyntaxRoot(newRoot);
        }


        private static ExpressionSyntax GetSimplifiedValue(SemanticModel semanticModel, BinaryExpressionSyntax binary, CancellationToken cancellationToken)
        {
            Optional<object>
                leftValue = semanticModel.GetConstantValue(binary.Left, cancellationToken),
                rightValue = semanticModel.GetConstantValue(binary.Right, cancellationToken)
            ;

            bool
                isLeftZero = leftValue.HasValue && leftValue.Value.Equals(0),
                isRightZero = rightValue.HasValue && rightValue.Value.Equals(0),
                isLeftOne = leftValue.HasValue && leftValue.Value.Equals(1),
                isRightOne = rightValue.HasValue && rightValue.Value.Equals(1)
            ;

            // +
            if (binary.IsKind(SyntaxKind.AddExpression))
            {
                // 0 + a
                if (isLeftZero)
                    return binary.Right;

                // a + 0
                if (isRightZero)
                    return binary.Left;
            }

            // -
            else if (binary.IsKind(SyntaxKind.SubtractExpression))
            {
                // 0 - a
                if (isLeftZero)
                    return SyntaxFactory.PrefixUnaryExpression(SyntaxKind.UnaryMinusExpression, binary.Right);

                // a - 0
                if (isRightZero)
                    return binary.Left;
            }

            // *
            else if (binary.IsKind(SyntaxKind.MultiplyExpression))
            {
                // 0 * a, a * 0
                if (isLeftZero ||
                    isRightZero)
                    return SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(0));

                // 1 * a
                if (isLeftOne)
                    return binary.Right;

                // a * 1
                if (isRightOne)
                    return binary.Left;
            }

            // /
            else if (binary.IsKind(SyntaxKind.DivideExpression))
            {
                // 0 / a
                if (isLeftZero)
                    return SyntaxFactory.LiteralExpression(SyntaxKind.NumericLiteralExpression, SyntaxFactory.Literal(0));

                // a / 1
                if (isRightOne)
                    return binary.Left;
            }

            return binary;
        }
    }
}
